#pragma once
#ifndef _KH_ENGINE_LEVEL_H
#define _KH_ENGINE_LEVEL_H
#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/Drawable.h>
#include <Kharlia/Engine/Units.h>
#include <Kharlia/Engine/TileData.h>
#include <vector>
#include <iostream>
#include <map>

#define COORDS_TO_TILE(tx, ty) (((tx % 16) + ((tx / 16) * 512)) + ((ty * 16) % 512))
#define TILE_TO_X(tilecode) ((tilecode % 16) + ((tilecode / 512) * 16))
#define TILE_TO_Y(tilecode) ((tilecode / 16) % 32)

namespace kharlia { namespace core {

class CTasker;

}}

namespace kharlia { namespace engine {

class FTileLayer;
class CGame;
class CWorld;
class CPlayer;

class CActor;
class APhysics;
class ARender;
class AMovement;
class APlayerController;

enum ENetRole;

// Tile coordinate encoding and decoding functions
FORCEINLINE
tile_t TileFromCoords(int tx, int ty) {
    return tile_t(((tx % 16) + ((tx / 16) * 512)) + ((ty * 16) % 512));
}

FORCEINLINE
int TileToX(tile_t code) {
    return int((code % 16) + ((code / 512) * 16));
}

FORCEINLINE
int TileToY(tile_t code) {
    return int((code / 16) % 32);
}

FORCEINLINE
tile_t TileFromVector2(const FVector2I& v) {
    return TileFromCoords(v.X, v.Y);
}

FORCEINLINE
FVector2I TileToVector2(tile_t code) {
    return FVector2I(TileToX(code),
                    TileToY(code));
}

class KH_ENGINE_DECLSPEC FTileLayer: public FDrawable {
    int mIndex;
    shared_ptr<FTileData> mTileData;

public:
    FTileLayer(int index);
    FTileLayer(int index, const FSizeI& size);
    FTileLayer(int index, shared_ptr<FTileData> tile_data);
    virtual ~FTileLayer();

    bool GetModified() { return GetRenderInfoInvalid(); }
    void SetModified(bool value) { SetRenderInfoInvalid(value); }

    const shared_ptr<FTileData>& GetTileData() { return mTileData; }
    int GetIndex() { return mIndex; }

    friend void _InitLevelClasses();
    friend void _InitLevelBindings();
    friend class CLevel;
    friend struct PSFTileLayer;
};

struct PSFTileLayer: public pickle_suite {
    static tuple getinitargs(FTileLayer&);
};

class KH_ENGINE_DECLSPEC FTileGroup {
public:
    typedef std::map<int, shared_ptr<FTileLayer>> LayersType;
    typedef LayersType::iterator LayerIterator;
    typedef LayersType::const_iterator ConstLayerIterator;

private:
    FSizeI mSize;
    LayersType mLayers;

public:
    FTileGroup(const FSizeI& size);
    virtual ~FTileGroup() {}

    void AddLayer(int index);
    void AddLayer(int index, shared_ptr<FTileLayer> tile_layer);

    const FSizeI& GetSize() const { return mSize; }
    int GetLayerCount() const { return mLayers.size(); }
    const shared_ptr<FTileLayer>& GetLayer(int index);
    std::vector<int> GetLayerIndices();
    list GetLayerIndicesPy();

    friend void _InitLevelClasses();
    friend void _InitLevelBindings();
    friend void _ExitLevelClasses();
    friend class CLevel;
    friend struct PSFTileGroup;

    static const uint8 WIDTH = 64;
    static const uint8 HEIGHT = 64;
};

struct PSFTileGroup: public pickle_suite {
    static tuple getinitargs(FTileGroup&);
    static bool getstate_manages_dict() { return true; }
    static tuple getstate(FTileGroup&);
    static void setstate(FTileGroup&, tuple);
};

struct KH_ENGINE_DECLSPEC FLayerInfo {
    int Index;
    unicode Name;
    union {
        struct {
            uint8 Red;
            uint8 Green;
            uint8 Blue;
            uint8 Alpha;
        };
        uint32 Color;
    };
    bool NoCollision;
    bool Visible;

    FLayerInfo():
        Index(0),
        Name(),
        Red(255),
        Green(255),
        Blue(255),
        Alpha(255),
        NoCollision(false),
        Visible(true) {}
};

struct PSFLayerInfo: public pickle_suite {
    static object getstate(FLayerInfo&);
    static void setstate(FLayerInfo&, str);
};

struct FTileInfo {
    uint8 Blocking: 1;
    uint8 Shape: 7;
    // Can't get pointer to bit field, make wrappers for python
    inline uint8 GetBlocking() const { return Blocking; }
    inline void SetBlocking(uint8 value) { Blocking = value; }
    inline uint8 GetShape() const { return Shape; }
    inline void SetShape(uint8 value) { Shape = value; }
};

struct FTileDef {
    FRectI Rect;
    str ImageName;
    std::vector<FTileInfo> TileInfo;
};

struct KH_ENGINE_DECLSPEC FTileset: public FDrawable {
    str Name;
    str ImageName;
    FSizeI Size;
    std::vector<FTileInfo> TileInfo;
    std::vector<FTileDef> TileDefs;

    FTileset() {}
    FTileset(str name, str imageName, const FSizeI& size):
        Name(name),
        ImageName(imageName),
        Size(size),
        TileInfo(size.Width * size.Height, FTileInfo()) {}
    
    bool CheckBlocking(uint16 code) const;
};

inline bool FTileset::CheckBlocking(uint16 code) const {
    return TileInfo[code].Blocking;
}

class KH_ENGINE_DECLSPEC CLevel: public core::CObject {
public:
    typedef std::map<int, shared_ptr<FLayerInfo>> LayerInfosType;
    typedef LayerInfosType::iterator LayerInfoIter;
    typedef LayerInfosType::const_iterator LayerInfoConstIter;
    typedef std::vector<shared_ptr<CActor>> ActorsType;
    typedef std::vector<shared_ptr<ARender>> RenderablesType;
    typedef std::vector<shared_ptr<FTileGroup>> TileGroupsType;

private:
    shared_ptr<CWorld> mWorld;
    unicode mName;
    object mNameHash;
    object mGUID;
    shared_ptr<core::FLogger> mLogger;
    float mTime;
    float mTimeDilation;
    uint32 mIDCounter;
    bool mIsInited;
    // Geometry
    FSizeI mSize, mTileGroupSize;
    std::vector<shared_ptr<FTileGroup>> mTileGroups;
    LayerInfosType mLayerInfos;
    shared_ptr<FTileset> mTileset;
    // Actors
    ActorsType mActors;
    //dict mActorMap; // name -> actor
    dict mActorIDMap; // hash(name) -> actor
    std::vector<shared_ptr<APhysics>> mCollidables;
    RenderablesType mRenderables;
    shared_ptr<core::CTasker> mActorTasker;

    void AddActor(shared_ptr<CActor> actor);
    void RemoveActor(shared_ptr<CActor> actor, uint32 id=0);

    static object msPlayerControllerClass;
    static object msActorClass;

    KH_DECL_GC()

public:
    static const uint8 PRE_TICK_GROUP = 0;
    static const uint8 TICK_GROUP = 1;
    static const uint8 POST_TICK_GROUP = 2;
    static const uint8 GROUP_COUNT = 3;

    CLevel(PyObject* self);
    ~CLevel();

    // Main
    void Init(shared_ptr<CWorld> world, object guid, unicode name, const FSizeI& size);
    void Exit();
    void Test(int num);
    void ActorTickerMain();
    void Tick(float deltaTime);
    void TickActors(float deltaTime);

    // Actors
    uint32 CreateID();
    object CreateActor(object components=object(),
                       shared_ptr<CActor> owner=shared_ptr<CActor>(),
                       shared_ptr<CActor> obj=shared_ptr<CActor>());
    void DestroyActor(shared_ptr<CActor> actor);
    void ClearActors();
    shared_ptr<APlayerController> CreatePlayerActor(shared_ptr<CPlayer>, ENetRole remoterole);
    void AddCollidable(shared_ptr<APhysics> actor);
    void RemoveCollidable(shared_ptr<APhysics> actor);
    bool AddRenderable(shared_ptr<ARender> r);
    bool RemoveRenderable(shared_ptr<ARender> r);
    void MoveActor(const shared_ptr<CActor>& actor,
                   const FVector3F& delta,
                   bool smooth=true,
                   bool collideTiles=true,
                   bool collideActors=true);

    // Geometry
    shared_ptr<FLayerInfo> CreateLayer(int Index);
    void SetTiles(int layer, tile_t tile);
    void SetTiles(int layer, const FVector2I& topos, shared_ptr<FTileData> tiledata);
    void SetTiles(int layer, const FVector2I& topos, const FSizeI& size, tile_t tile);
    void SetTiles(int layer, const FVector2I& topos, const FSizeI& size, object sequence);
    void SetTiles(int layer, const FVector2I& topos, const FSizeI& size, const tile_t* tiles);
    bool CollideTiles(const FRectF& dest, int layer);

    // Events
    virtual void OnGameStarted(shared_ptr<CGame> game);
    virtual void OnGameStopped();
    void OnCreated();
    void OnDestroyed();
    void _OnCreated();
    void _OnDestroyed();

    // Accessors
    KH_DEF_GET(const unicode&, Name)
    KH_DEF_GET(const object&, NameHash)
    sint32 GetNameHashAsInt() const;
    KH_DEF_GET(const object&, GUID)
    KH_DEF_GET(const shared_ptr<CWorld>&, World)
    KH_DEF_GET(bool, IsInited)
    KH_DEF_GET(float, Time)
    KH_DEF_GETSET(float, TimeDilation)
    KH_DEF_GET(const shared_ptr<core::FLogger>&, Logger)
    KH_DEF_GET(const FSizeI&, Size)
    KH_DEF_GET(const FSizeI&, TileGroupSize)
    KH_DEF_GET(const ActorsType&, Actors)
    KH_DEF_GET(const dict&, ActorIDMap)
    KH_DEF_GET(const shared_ptr<core::CTasker>&, ActorTasker)
    KH_DEF_GET(const RenderablesType&, Renderables)
    KH_DEF_GET(const LayerInfosType&, LayerInfos)
    int GetTileGroupCount() const { return mTileGroups.size(); }
    int GetActorCount() const { return mActors.size(); }
    shared_ptr<CActor> GetActorByTag(object name) const;
    shared_ptr<CActor> GetActorByID(uint32 id) const;
    const shared_ptr<CActor>& GetActorByIndex(int index) const { return mActors[index]; }

    const shared_ptr<FTileGroup>& GetTileGroup(uint index) const;
    const shared_ptr<FTileGroup>& GetTileGroup(const FVector2I& pos) const;
    const shared_ptr<FTileGroup>& GetTileGroupAt(const FVector2I& pos) const;
    const shared_ptr<FTileGroup>& GetTileGroupAt(const FVector2F& pos) const;
    const FTileInfo* GetTileInfo(int layer, const FVector2I& pos) const;
    const FTileInfo* GetTileInfo(int tilecode) const;

    int GetLayerInfoCount() { return mLayerInfos.size(); }
    list GetLayerInfoIndicesPy();
    shared_ptr<FLayerInfo> GetLayerInfo(int index);
    bool CheckLayerExists(int index);

    static const uint8 MAX_WIDTH = 0xFF;
    static const uint8 MAX_HEIGHT = 0xFF;

    static void SetPlayerControllerClass(object cls);
    static object GetActorClass();
    static void SetActorClass(object cls);

    friend void _InitLevelClasses();
    friend void _InitLevelBindings();
    friend void _ExitLevelClasses();
    friend struct PSCLevel;
};

inline object CLevel::GetActorClass() {
    return msActorClass;
}

inline sint32 CLevel::GetNameHashAsInt() const {
    return static_cast<sint32>(PyInt_AS_LONG(mNameHash.ptr()));
}

inline void CLevel::OnCreated() {
    call_method<void>(mSelf, "OnCreated");
}

inline void CLevel::OnDestroyed() {
    call_method<void>(mSelf, "OnDestroyed");
}

inline const shared_ptr<FTileGroup>& CLevel::GetTileGroup(uint index) const { 
    return mTileGroups.at(index);
}

inline const shared_ptr<FTileGroup>& CLevel::GetTileGroup(const FVector2I& pos) const {
    return GetTileGroup((mSize.Width * pos.Y) + pos.X);
}

inline const shared_ptr<FTileGroup>& CLevel::GetTileGroupAt(const FVector2I& pos) const {
    return GetTileGroup(FVector2I(pos.X / mTileGroupSize.Width,
                                  pos.Y / mTileGroupSize.Height));
}

inline const shared_ptr<FTileGroup>& CLevel::GetTileGroupAt(const FVector2F& pos) const {
    return GetTileGroup(FVector2I(int(pos.X) / mTileGroupSize.Width,
                                  int(pos.Y) / mTileGroupSize.Height));
}

inline const FTileInfo* CLevel::GetTileInfo(int layer, const FVector2I& pos) const {
    if (!mTileset)
        return NULL;
    const shared_ptr<FTileGroup>& tg = GetTileGroupAt(pos);
    const shared_ptr<FTileLayer>& tl = tg->GetLayer(layer);
    uint16 idx = (mTileGroupSize.Width * (pos.Y % FTileGroup::WIDTH)) +
                    (pos.X % FTileGroup::HEIGHT);
    tile_t code = tl->GetTileData()->GetTiles().at(idx);
    return &mTileset->TileInfo.at(code);
}

inline const FTileInfo* CLevel::GetTileInfo(int tilecode) const {
    if (!mTileset)
        return NULL;
    return &mTileset->TileInfo.at(tilecode);
}

inline shared_ptr<FLayerInfo> CLevel::GetLayerInfo(int index) {
    LayerInfoIter it = mLayerInfos.find(index);
    return it == mLayerInfos.end()? shared_ptr<FLayerInfo>(): it->second;
}

inline bool CLevel::CheckLayerExists(int index) {
    return mLayerInfos.find(index) != mLayerInfos.end();
}

inline list CLevel::GetLayerInfoIndicesPy() {
    list out;
    for (LayerInfoIter it = mLayerInfos.begin(); it != mLayerInfos.end(); it++) {
        out.append(it->first);
    }
    return out;
}

inline void CLevel::ClearActors() {
    call_method<void>(mSelf, "ClearActors");
}

struct PSCLevel: public pickle_suite {
    static bool getstate_manages_dict() { return true; }
    static tuple getstate(CLevel&);
    static void setstate(CLevel&, tuple);
};

}} //namespace Kharlia

KH_HAS_BACK_REFERENCE(kharlia::engine::CLevel)

#endif